<?php
/**
 * Created by PhpStorm.
 * User: whwyy
 * Date: 2018/3/30 0030
 * Time: 14:56
 */

namespace Database;

trait TraitModel
{

    public $where = [];
    public $select = [];
    public $join = [];
    public $order = [];
    public $offset = NULL;
    public $limit = NULL;
    public $group = '';
    public $from = '';
    public $alias = 't1';

    /**
     * @param string $alias
     *
     * @return $this
     *
     * select * from tableName as t1
     */
    public function alias($alias = 't1')
    {
        $this->alias = $alias;
        return $this;
    }

    /**
     * @param $tableName
     *
     * @return $this
     *
     */
    public function from($tableName)
    {
        $this->from = $tableName;
        return $this;
    }

    /**
     * @param array|string $tableName
     * @param              $on
     * @param null $param
     *
     * @return $this
     * $query->join([$tableName, ['userId'=>'uuvOd']], $param)
     * $query->join([$tableName, ['userId'=>'uuvOd'], $param])
     * $query->join($tableName, ['userId'=>'uuvOd',$param])
     */
    private function join($tableName, $on = NULL, $param = NULL)
    {
        switch (func_num_args()) {
            case 1:
                return $this->resolveJoinArray($tableName);
                break;
            case 2:
                $join = [];
                if (is_array($tableName)) {
                    if (!is_string(current($tableName))) {
                        return $this;
                    }
                    $join[] = array_shift($tableName);
                    $join[] = 'ON ' . $this->toString($tableName);
                } else {
                    $join[] = $tableName;
                    $join[] = 'ON ' . $this->toString($on);
                }
                $this->join[] = implode(' ', $join);
                return $this;
                break;
            case 3:
                $join[] = $tableName;
                $join[] = "ON " . key($on) . '=' . array_shift($on);
                if (!empty($param)) {
                    $join[] = ' AND ' . $this->toString($param);
                }
                $this->join[] = implode(' ', $join);
                return $this;
                break;
            default:
                return $this;
        }
    }

    /**
     * @param      $tableName
     * @param      $onCondition
     * @param null $param
     *
     * @return $this
     */
    public function leftJoin($tableName, $onCondition, $param = NULL)
    {
        return $this->join(...["LEFT JOIN " . $tableName, $onCondition, $param]);
    }

    /**
     * @param      $tableName
     * @param      $onCondition
     * @param null $param
     *
     * @return $this
     */
    public function rightJoin($tableName, $onCondition, $param = NULL)
    {
        return $this->join(...["RIGHT JOIN " . $tableName, $onCondition, $param]);
    }

    /**
     * @param      $tableName
     * @param      $onCondition
     * @param null $param
     *
     * @return $this
     */
    public function innerJoin($tableName, $onCondition, $param = NULL)
    {
        return $this->join(...["INNER JOIN " . $tableName, $onCondition, $param]);
    }

    /**
     * @param array $array
     *
     * @return $this
     */
    private function resolveJoinArray(array $array)
    {
        if (count($array) < 2) {
            return $this;
        }
        $table = current($array) . ' ON ';
        unset($array[key($array)]);
        $join = [];
        foreach ($array as $key => $val) {
            if (empty($join)) {
                $join[] = "$key=$val";
            } else {
                $join[] = $this->toString($val);
            }
        }
        $this->join[] = 'INNER JOIN ' . $table . implode(' AND ', $join);
        return $this;
    }

    /**
     * @param $array
     *
     * @return string
     */
    private function toString($array)
    {
        $tmp = [];
        if (!is_array($array)) {
            return $array;
        }
        foreach ($array as $key => $val) {
            if (is_array($val)) {
                $_tmp = $this->toString($array);
                if (!empty($_tmp)) {
                    continue;
                }
                $tmp[] = $_tmp;
            } else {
                $tmp[] = $this->resolve($key, $val, '=');
            }
        }
        return implode(' AND ', $tmp);
    }

    /**
     * @param $field
     *
     * @return $this
     */
    public function sum($field)
    {
        $this->select[] = 'SUM(' . $field . ')';
        return $this;
    }

    /**
     * @param $field
     * @return $this
     */
    public function max($field)
    {
        $this->select[] = 'MAX(' . $field . ') AS ' . $field;
        return $this;
    }

    /**
     * @param string $lngField
     * @param string $latField
     * @param int $lng1
     * @param int $lat1
     *
     * @return $this
     */
    public function distance(string $lngField, string $latField, int $lng1, int $lat1)
    {
        $sql = "ROUND(6378.138 * 2 * ASIN(SQRT(POW(SIN(($lat1 * PI() / 180 - $lat1 * PI() / 180) / 2),2) + COS($lat1 * PI() / 180) * COS($latField * PI() / 180) * POW(SIN(($lng1 * PI() / 180 - $lngField * PI() / 180) / 2),2))) * 1000) AS distance";
        $this->select[] = $sql;
        return $this;
    }

    /**
     * @param        $column
     * @param string $sort
     *
     * @return $this
     *
     * [
     *     'addTime',
     *     'descTime desc'
     * ]
     */
    public function orderBy($column, $sort = 'DESC')
    {
        if (empty($column)) {
            return $this;
        }
        if (is_string($column)) {
            return $this->addOrder(...func_get_args());
        }

        foreach ($column as $key => $val) {
            $this->addOrder($val);
        }

        return $this;
    }

    /**
     * @param        $column
     * @param string $sort
     *
     * @return $this
     *
     */
    private function addOrder($column, $sort = 'DESC')
    {
        $column = trim($column);

        if (func_num_args() == 1 || strpos($column, ' ') !== FALSE) {
            $this->order[] = $column;
        } else {
            $this->order[] = "$column $sort";
        }
        return $this;
    }

    /**
     * @param array|string $column
     *
     * @return $this
     */
    public function select($column = '*')
    {
        if (!is_array($column)) {
            $column = explode(',', $column);
        }
        foreach ($column as $key => $val) {
            array_push($this->select, $val);
        }
        return $this;
    }

    /**
     * @param        $columns
     * @param string $oprea
     * @param null $value
     *
     * @throws \Exception
     * @return $this|array|Model
     */
    public function whereOr($columns, $oprea = '=', $value = NULL)
    {

        if (!empty($this->where)) {
            $where = '(' . implode(' AND ', $this->where) . ')';
        } else {
            $where = '';
        }
        $this->where = [];
        if (is_array($columns)) {
            $this->addArrayCondition($columns);
        } else if (func_num_args() == 3) {
            $this->mathematics($columns, $oprea, $value);
        } else if (is_numeric($columns)) {
            $this->where[] = $this->numeric($columns);
        } else if (is_array($oprea)) {
            $tmp = [];
            foreach ($oprea as $key => $val) {
                $tmp[] = $this->resolve($val, $columns, '=');
            }
            $this->bingQuery(implode(' OR ', $tmp));
        } else {
            $this->bingQuery($this->resolve($columns, $oprea, '='));
        }
        $implode = implode(' AND ', $this->where);
        if (empty($where)) {
            $this->where = ["{$implode}"];
        } else {
            if (count($this->where) == 1) {
                $this->where = ["({$where}) OR {$implode}"];
            } else {
                $this->where = ["({$where}) OR ({$implode})"];
            }
        }

        return $this;
    }


    /**
     * @param $numeric
     *
     * @return string
     * @throws \Exception
     */
    private function numeric($numeric)
    {
        /** @var ActiveRecord $model */
        $model = $this->getModel();
        return $model->getPrimary() . '=' . $numeric;
    }

    /**
     * @param        $columns
     * @param string $oprea
     * @param null $value
     *
     * @throws \Exception
     * @return array|Model|mixed
     */
    public function whereAnd($columns, $oprea = '=', $value = NULL)
    {
        if (is_array($columns)) {
            return $this->addArrayCondition($columns);
        }

        if (func_num_args() == 3) {
            return $this->mathematics($columns, $oprea, $value);
        }

        $string = '';
        if (is_numeric($columns)) {
            $this->numeric($columns);
        } else {
            $string = $this->resolve($columns, $oprea, $value);
        }
        return $this->bingQuery($string);
    }

    /**
     * @param array|string $columns
     * @param string $value
     *
     * @return $this
     */
    public function whereLike($columns, string $value)
    {

        if (empty($columns) || empty($value)) {
            return $this;
        }

        if (is_array($columns)) {
            $columns = 'CONCAT(' . implode(',^,', $columns) . ')';
        }

        $this->where[] = "{$columns} like '%{$value}%'";

        return $this;
    }

    /**
     * @param $columns
     * @param $value
     *
     * @return $this
     */
    public function whereIn($columns, $value)
    {
        if (empty($value)) {
            return $this;
        }
        if (is_array($value)) {
            $tmp = [];
            foreach ($value as $key => $val) {
                if (is_numeric($val)) {
                    $tmp[] = $val;
                } else {
                    $tmp[] = '\'' . $val . '\'';
                }
            }
        } else {
            $tmp = [$value];
        }

        $tmp = implode(',', $tmp);

        $this->where[] = "{$columns} in ({$tmp})";

        return $this;
    }

    /**
     * @param string $column
     * @param string $start
     * @param string $end
     *
     * @return $this
     */
    public function whereBetween(string $column, string $start, string $end)
    {
        if (empty($column) || empty($start) || empty($end)) {
            return $this;
        }

        if (is_string($start) || is_string($end)) {
            $this->where[] = "$column between '$start' '$end'";
        } else {
            $this->where[] = "$column between $start $end";
        }
        return $this;
    }

    /**
     * @param array $params
     *
     * @return $this
     */
    public function bindParams(array $params = [])
    {
        if (empty($params)) {
            return $this;
        }
        $this->attributes = $params;
        return $this;
    }

    /**
     * @param        $columns
     * @param string $oprea
     * @param null $value
     * @param string $or
     *                  [field, opreat, value]
     *
     * @return $this|array|Model|string
     * @throws \Exception
     */
    public function where($columns, $oprea = '=', $value = NULL, $or = 'AND')
    {
        if (is_array($columns) && count($columns) >= 1) {
            return $this->addArrayCondition($columns);
        }

        if (func_num_args() == 3) {
            return $this->mathematics($columns, $oprea, $value);
        }

        if (is_numeric($columns)) {
            $id = $this->getModel()->getPrimary();
            if (empty($id)) {
                throw new \Exception("Table not hav primary key");
            }
            $string = $this->resolve($id, $columns, '=');
        } else if (func_num_args() > 1) {
            $string = $this->resolve($columns, $value, $oprea);
        } else {
            $string = $columns;
        }
        if (empty($string)) {
            return $this;
        }
        if (strtolower($or) != 'or' || empty($this->where)) {
            return $this->bingQuery($string);
        }

        $where = '( ' . implode(' AND ', $this->where) . ' )';

        $this->where = ["{$where} OR {$string}"];

        return $this;
    }

    /**
     * @param string $name
     *
     * @return $this
     */
    public function groupBy(string $name, $having = NULL)
    {
        $this->group = $name;
        if (!empty($having)) {
            if (is_array($having)) {
                $query = [];
                foreach ($having as $key => $val) {
                    $query[] = $this->resolve($key, $val, '=');
                }
                $this->group .= ' HAVING ' . implode(' AND ', $query);
            } else {
                $this->group .= ' HAVING ' . $having;
            }
        }
        return $this;
    }

    /**
     * @param int $offset
     * @param int|null $limit
     *
     * @return $this
     */
    public function limit(int $offset, int $limit = 20)
    {
        if (func_num_args() == 2) {
            $this->limit = $offset . ',' . $limit;
        } else {
            $this->limit = $offset;
        }
        return $this;
    }

    /**
     * @param array $array
     *
     * @return $this
     */
    private function addArrayCondition(array $array)
    {
        if (empty($array)) {
            return $this;
        }
        foreach ($array as $key => $val) {
            if ($val === NULL) continue;
            if (is_array($val)) {
                $this->addArrayCondition($val);
            } else if (strpos($key, '<') !== FALSE) {
                $this->bingQuery($key . ' ' . $val);
            } else if (strpos($key, '>') !== FALSE) {
                $this->bingQuery($key . ' ' . $val);
            } else if (strpos($key, '=') !== FALSE) {
                $this->bingQuery($key . ' ' . $val);
            } else {
                $this->bingQuery($this->resolve($key, $val));
            }
        }
        return $this;
    }


    /**
     * @param $column
     * @param $value
     * @param $oprea
     *
     * @return string
     *
     * $query = new QueryBuilder();
     * $query->where('id', '2');
     * $query->where(['id' => 3]);
     * $query->where('id', '<', 4);
     * $query->orWhere('id', '=', 5);
     * $query->orWhere('id', '=', 6);
     * $query->ANDWhere('id', '=', 7);
     * $sql = '(((id=2 AND id=3 AND id<4) OR id=5) OR id=6) AND i(d=7)';
     */
    protected function resolve($column, $value, $oprea = '=')
    {
        if ($value === NULL || $value === '') {
            return '';
        }
        if (is_numeric($value)) {
            $str = $column . $oprea . $value;
        } else {
            $str = $column . $oprea . '\'' . $value . '\'';
        }
        return $str;
    }

    /**
     * @param $column
     * @param $oprea
     * @param $value
     *
     * @return $this|Model
     */
    private function mathematics($column, $oprea, $value)
    {
        $mathematics = ['<', '>', '<=', '>=', '<>'];

        if (!in_array($oprea, $mathematics)) {
            return $this;
        }

        if (is_numeric($value)) {
            return $this->bingQuery("{$column} {$oprea} {$value}");
        } else {
            return $this->bingQuery("{$column} {$oprea} '{$value}'");
        }
    }

    /**
     * @param $string
     *
     * @return $this
     */
    private function bingQuery($string)
    {
        if (empty($string)) {
            return $this;
        }
        $this->where[] = $string;
        return $this;
    }
}